Java 之 JDK 8 新特性

  在 JDK8 中,新增了许多新特性,可以让开发人员更优雅的编写代码,那么下面跟随本文来了解一下吧!

Lambda 表达式

  在 JDK8 中,新增的 Lambda 表达式可以大大地简化了我们的代码量,其使用起来也非常简单。
  在使用之前,我们先了解下函数式的编程思想。

什么是函数式的编程思想?

  在数学中,函数是有输入量、输出量的一套计算方案,即“拿什么东西做什么事情”
  在面向对象中,过分强调着“必须通过对象的形式来做事情“,而函数式思想则尽量忽略该概念,强调做什么,却不注重它以什么形式做。
  因此,引出一个新的名词——函数式接口
  函数式接口:即有且只有一个抽象方法的接口。
  如何标注一个接口为函数式接口呢?
  在 Java 中,可以用@FunctionalInterface注解标识该接口是否函数式接口,是才能编译成功。
  那么,为什么要了解什么是函数式接口呢?
  这是因为,Lambda 表达式使用的前提就是其作用的接口必须为函数式接口

传统的线程代码

  若我们想使用一个线程,方法之一是实现 Runnable 接口然后再将其作为参数传入。
  在传统的做法中,我们有 2 种实现方案:

  • 接口实现类
  • 匿名内部类

接口实现类线程实现

  首先我们需要创建一个 Runnable 接口并重写 run()方法:

1
2
3
4
5
6
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 这个新线程被创建了");
}
}

  其次将接口实现类作为 Thread 的参数,然后调用start()方法即可:

1
2
3
4
new Thread(new RunnableImpl()).start();
// 与下面这两句等效哦
// Runnable r = new RunnableImpl();
// new Thread(r).start();

  测试结果:

1
Thread-0 这个新线程被创建了

匿名内部类线程实现

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 这个新线程被创建了");
}
}).start();

  测试结果与第一种做法相同。

思考

  阅读前述代码时,我们会发现,有许多冗余代码并不需要被关注。
  为何不需要被关注呢?
  因为此种接口只有一个方法,使用该接口肯定知道这个接口及其方法是什么,你还展示给我看干嘛!这信息对我无用啊,我已经知道了呀!
  那 Lambda 表达式就说了:“我~来~帮~你~把~它~隐藏掉!”
  这里涉及到一个编程思想的转换,重点关注做什么,而不是怎么做
  我们真的希望创建一个匿名内部类嘛?
  不,我们只是为了做这件事情而不得不创建一个对象,我们真正希望做的事情是:run()方法内的代码传递给Thread类知晓,,因为调用Thread类的start()方法时其实调用的是run()方法。

  传递一段代码——这才是我们的真正目的,而创建对象只是受限于面向对象语法而不得不采用的一种手段。
  那么,是否存在一种更简单的办法呢?
  自然是有的,使用 Lambda 表达式就好了,下面我们来使用它吧!!!

Lambda 表达式的线程实现

1
2
3
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "这个新线程被创建了");
}).start();

  此方式的运行结果与前两种做法是一样的,却更为简单优雅。
  既然这么好用,那 Lambda 表达式到底是个什么东东呢?标准定义又是怎样的呢?

Lambda 表达式标准格式

  在Lambda 表达式中,省去了面向对象的条条框框,其格式由三个部分组成:

  • 一个()及其中的一些参数(也可以无参数)
  • 一个箭头->
  • 一个大括号{}及一段本来写在重写方法中的代码

  因此,Lambda 表达式的标准格式为:

1
(参数类型 参数名称)->{代码语句}

示例:加深 Lambda 表达式理解

无参数无返回值的自定义函数式接口

  创建一个Student接口,之后将接口作为参数传递调用其方法:

1
2
3
4
// 接口
public interface Student {
void work();
}

  下面分别为匿名内部类和 Lambda 表达式的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 将接口作为参数传递调用其方法
public void invokeWork(Student student){
student.work();
}

// 匿名内部类
@Test
public void test1{
invokeWork(new Student() {
@Override
public void work() {
System.out.println("学生的任务是学习");
}
});
}

// Lambda 表达式
public void test2{
invokeWork(()->{
System.out.println("学生的任务是学习");
});
}

  测试结果:

1
学生的任务是学习

有参数有返回值函数式接口

  需求:

  • 创建Teacher对象
  • 通过Arrays.sort()方法对不同Teacher对象按年龄大小排序(重写Comparator接口排序规则)
1
2
3
4
5
6
7
8
9
10
11
12
public class Teacher {
private String name;
private int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
// getter、setter、toString 方法省略
}

  下面分别为匿名内部类和 Lambda 表达式的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Before
public void before{
Teacher[] teachers = {new Teacher("王老师",18),
new Teacher("李老师",20),
new Teacher("赵老师",22)};
}

// 匿名内部类
@Test
public void test1(){
Arrays.sort(teachers, new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
return o1.getAge() - o2.getAge();
}
});
}

// Lambda 表达式
@Test
public void test2(){
Arrays.sort(teachers,(Teacher o1,Teacher o2)->{
return o1.getAge() - o2.getAge();
});
}
@After
public void after{
for (Teacher teacher : teachers) {
System.out.println(teacher);
}
}

  运行结果:

1
2
3
Teacher{name='王老师', age=18}
Teacher{name='李老师', age=20}
Teacher{name='赵老师', age=22}

有参数有返回值的自定义函数式接口

  编写一个用于求和的接口:

1
2
3
public interface Sum {
int sumNumber(int a,int b);
}

  下面分别为匿名内部类和 Lambda 表达式求和的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int invokeSum(int a, int b, Sum sum) {
int result = sum.sumNumber(a, b);
return result;
}

// 匿名内部类
@Test
public void testSum1() {
int r = invokeSum(66, 88, new Sum() {
@Override
public int sumNumber(int a, int b) {
return a + b;
}
});
System.out.println(r);
}

// Lambda 表达式
@Test
public void testSum2() {
int r = invokeSum(66, 88, (int a,int b) -> {
return a + b;
}
);
System.out.println(r);
}

  运行结果:

1
154

  与匿名内部类相比,Lambda 表达式除了可以简化代码,其编译的代码还无匿名内部类的标识$呢,如:

1
2
Demo$1.class  // 匿名内部类方式编译完的代码
Demo.class // Lambda 表达式方式编译完的代码

Lambda 表达式的进一步省略写法

  对 Lambda 表达式而言,它是可推导可省略的,凡是根据上下文推导出来的内容,都可以省略书写,可省略内容包括:

  • (参数列表):括号中参数列表的数据类型可以省略不写
  • (参数列表):若括号中的参数只有一个,那么类型和参数都可以省略不写
  • {代码}:若{}中的代码只有一行,则无论是否有返回值,其中的return{}及省略的}前的;都可以省略不写。

  下面使用更省略的 Lambda 表达式重写前面的例子:

1
2
3
4
5
6
7
8
9
10
11
// 线程
new Thread(() ->System.out.println(Thread.currentThread().getName() + "这个新线程被创建了")).start();

// 学生接口
invokeWork(() -> System.out.println("学生的任务是学习"));

// 按年龄排序教师
Arrays.sort(teachers, (o1, o2) -> o1.getAge() - o2.getAge());

// 求和
int r = invokeSum(66, 88, (a,b) -> a + b);

Lambda 的延迟执行

  有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。
  而 Lambda 表达式具备延迟执行的特性,正好可以解决该问题。
  假如现在我们要根据条件拼接字符串,条件正确时才能拼接字符串,如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UnDelay{
private String s1 = "hello ";
private String s2 = "world";

public void splitByNumber(int number, String msg) {
if (number == 1) {
System.out.println(msg);
}
}

@Test
public void unDelay() {
// 不论条件是否正确都会拼接字符串
splitByNumber(1, s1 + s2);
splitByNumber(2, s1 + s2);
}
}

  在这种情况下,不论条件是否正确,s1s2字符串都会拼接,浪费了部分性能。
  而使用 Lambda 表达式只会在条件正确时才执行拼接,下面为修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 接口
public interface SplitMsg {
String splitMessage();
}
// 测试代码
public class Delay{
private String s1 = "hello ";
private String s2 = "world";
public void splitByLambda(int number, SplitMsg splitMsg) {
if (number == 1) {
System.out.println(splitMsg.splitMessage());
}
}

@Test
public void Delay() {
// 条件正确则拼接,失败则不执行
splitByLambda(1, () -> {
System.out.println("条件正确你才能看到该代码");
return s1 + s2;
});
splitByLambda(2, () -> {
System.out.println("条件正确你才能看到该代码");
return s1 + s2;
});
}
}

  最后的输出结果:

1
2
条件正确你才能看到该代码
hello world

常用的函数式接口

  除Runnable接口与Comparator<T>接口外,JDK 中还提供了许多常用的函数式接口,它们主要分布在java.util.function包中。

Supplier <T>

  Supplier <T> 接口进包含一个无参的方法T get(),用来获取一个泛型参数指定类型的对象数据,即生产一个数据。
  比如我们可以将泛型指定为字符串类型,重写方法中返回自定义的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestSupplier {
public String getString(Supplier<String> supplier) {
return supplier.get();
}

@Test
public void testGetString() {
String s = getString(() -> "返回该字符串");
System.out.println(s);
}
}
// 运行结果
返回该字符串

  当然我们也可以获取 int 类型数据,获取一个数组的最小值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestSupplier {
public int getMinNumber(Supplier<Integer> supplier) {
return supplier.get();
}

@Test
public void testGetMinNumber() {
int[] arr = {12, 48, 92, 8, 22, 3, 99, 1};
int min = getMinNumber(() -> {
int m = arr[0];
for (int i = 1; i < arr.length; i++) {
if (m > arr[i]) {
m = arr[i];
}
}
return m;
});
System.out.println(min);
}
}
// 运行结果
1

Consumer<T>

  Consumer<T> 接口正好与 Supplier <T> 接口相反,它不是生产一个数据,而是消费一个数据,数据类型也由泛型 T 决定。该接口包含两个方法:

  • void accept(T t);:消费传递进来的数据 T
  • andThen(Consumer<? super T> after):拼接两个 Consumer<T> 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    // 消费一次数据
public void consumer(String name, Consumer<String> consumer) {
consumer.accept(name);
}

@Test
public void test1() {
consumer("zhangsan", (name) -> {
System.out.println(name);
System.out.println(name.toUpperCase());
System.out.println(name.length());
});
}
// 多次消费数据
public void splitConsumer(String s, Consumer<String> con1, Consumer<String> con2) {
con1.accept(s);
con2.accept(s);
// con1.andThen(con2);//拼接多次消费,等效于前面两句,con1写在前面先被消费
}

@Test
public void test2() {
splitConsumer("AsfsSJFjfjAof",
s -> {
System.out.println(s.toUpperCase());
},
s -> {
System.out.println(s.toLowerCase());
});
}

Predicate<T>

  某些情况下需要对某种数据类型的数据进行判断,从而得到一个boolean结果,这时可以使用 Predicate<T> 接口,该接口包含以下方法:

  • boolean test(T t):根据其中重写代码判断传递的数据返回为truefalse
  • Predicate<T> and(Predicate<? super T> other): 将两个 Predicate 连接起来比较,相当于&&
  • Predicate<T> or(Predicate<? super T> other):相当于||
  • Predicate<T> negate():相当于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    //测试test方法
public boolean getBoolean(String s, Predicate<String> predicate) {
return predicate.test(s);
}

@Test
public void test() {
boolean b = getBoolean("wangwu", s ->
s.length() == 6);
System.out.println(b);
}

// 测试 and 方法
public boolean getAnd(String s, Predicate<String> predicate1, Predicate<String> predicate2) {
return predicate1.test(s) && predicate2.test(s);
// 下面的代码与上面等效
// return predicate1.and(predicate2).test(s);
}

@Test
public void test2() {
boolean b = getAnd("zhaoliu",
s -> s.length() == 7,
s -> s.startsWith("z")
);
System.out.println(b);
}

  ornegate方法与and方法类似。

Function<T,R>

  Function<T,R> 接口用来将一个类型的数据转换为另一个类型的数据,前者称为前置条件,后者称为后置条件。
  该接口主要包含下面 2 个重要的方法:

  • R apply(T t);:将 T 类型的数据转换为 R 类型的数据
  • Function<T, V> andThen(Function<? super R, ? extends V> after):类似于 Consumer 接口的andThen方法,拼接多个 Function 接口,可以进行多次转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public int getTranslate(String s , Function<String,Integer> function){
return function.apply(s);
}

@Test
public void test(){
int a = getTranslate("12312",s -> Integer.parseInt(s));
System.out.println(a);
}

/**
* 进行2次转换,第一次将字符串转换为数字,第二次将字符串转换为字符串
* @param s
* @param function1
* @param function2
* @return
*/
public String getTwiceTranslate(String s,Function<String,Integer> function1,Function<Integer,String> function2){
return function1.andThen(function2).apply(s);
}

@Test
public void test2(){
String result = getTwiceTranslate("123414",
s -> Integer.parseInt(s) + 100000,
s -> s.toString()
);
System.out.println(result);
}

注意事项

  前面也提到过,使用 Lambda 表达式一定要注意接口必须为函数式接口

Stream 流

  我们先通过一个集合元素过滤的例子来引入 Stream 流的概念。

例子:集合元素的过滤

  集合元素的过滤可以使用传统的增强for循环方式遍历,

传统方式

  若需要对集合中的元素进行过滤,如对“王”姓及姓名长度为 2 的人进行过滤,传统代码需要 2 步:

  • 先将集合 A 根据条件一过滤为子集 B
  • 然后再根据条件二过滤为子集 C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void test() {
List<String> list = new ArrayList<>();
list.add("王五");
list.add("李四");
list.add("赵六");
list.add("刘七");
list.add("王刘");
list.add("王老五");
List<String> filter1 = new ArrayList<>();
// 第一次过滤:只保留“王”姓的人
for (String s : list) {
if (s.startsWith("王")) {
filter1.add(s);
}
}
List<String> filter2 = new ArrayList<>();
// 第二次过滤:只保留姓名长度为 2 的人
for (String s : filter1) {
if(s.length() == 2){
filter2.add(s);
}
}
// 遍历输出
for (String s : filter2) {
System.out.println(s);
}
}

  测试结果:

1
2
王五
王刘

  从测试结果可以发现:循环遍历操作非常麻烦
  前面说过,Lambda 表达式可以让我们更加专注做什么,而不是怎么做

  但从上面的代码发现:

  • for 循环的做法就是“怎么做”
  • for 循环的循环体才是“做什么”

  为什么使用循环?
  因为要进行遍历,但循环是遍历的唯一方式吗?
  遍历是指将每一个元素从集合中取出并逐一进行处理(不一定按顺序),而并不是从某个位置到某个位置顺次处理的循环。
  前者(遍历)是目的,后者(循环)是方式。

  一方面,每当需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。
  这是理所当然的吗?
  当然不是,循环是做事情(遍历)的方式(怎么做),而不是目的(做什么)。
  另一方面,使用线性循环就意味着只能遍历一次。若希望再次遍历,只能再用另一个循环从头开始。
  但是, Stream 流方式却有更优雅的做法。

Stream 流方式

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test2() {
List<String> list = new ArrayList<>();
list.add("王五");
list.add("李四");
list.add("赵六");
list.add("刘七");
list.add("王刘");
list.add("王老五");
list.stream().filter(name -> name.startsWith("王")).filter(name -> name.length() == 2).forEach(name -> System.out.println(name));
}

  测试结果与传统方式相同,但代码量却大大减少。

  那么,到底什么是 Stream 流?

什么是 Stream 流?

  要想回答这个问题,得先明白流式思想的概念,流式思想类似于工厂车间的生产流水线
  如啤酒的加工过程一般分为以下几步:

  • 瓶子分类
  • 洗涤
  • 装酒
  • 封口
  • 装箱
  • 打包

  当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们首先应该创建一个模型,然后按模型中的方案去执行它。
  对 Stream 流来说,其过程如下:


  上图展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种函数模型,图中的每一个方框都是一个,调用指定的方法,可以从一个流模型转换为另一个流模型,而最右侧的数字3便是最终结果。
  上面的filtermapskip都是在对函数模型进行操作,集合元素并没有被真正处理,只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作,这得益于 Lambda 的延迟执行特性。

  “Stream 流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

  Stream(流)是一个来自数据源的元素队列:

  • 元素:特定类型的对象,形成一个队列,Java 中的 Stream 并不会存储元素,而是按需计算;
  • 数据源:流的来源,可以是集合,数组等等;
  • Pipelining:中间操作都会返回流对象本身,这样多个操作便可以串成一个管道,如同流式风格。这样做可以对操作进行优化,比如延迟执行和短路。
  • 内部迭代:以前对集合遍历都是通过Iterator或者增强for方式遍历,显式的在集合外部进行迭代,叫做外部迭代。而 Stream 提供了内部迭代,其可以直接调用遍历方法。

  当使用一个流的时候,通常包括三个基本步骤:

  • 获取一个数据源
  • 数据转换
  • 执行操作得到想要的结果

  每次转换时原有 Stream 对象不变,但返回一个新的 Stream 对象,并且可以多次转换哟。

Stream 流获取的两种方式

  java.util.stream.Stream<T>是 Java 8 新加入的最常用的流接口。
  要想获取一个流非常简单:

  • 对所有的Collection集合,都可以通过stream默认方法获取流;
  • 对数组而言,可通过Stream流接口的静态方法of获取流。

根据 Colleantion 获取流

  所有的Colleantion集合都有一个stream方法,通过该方法便可以获取流,其源码如下:

1
2
3
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}

  当然,根据集合的不同,获取流的代码可能略微存在差异。

List 集合获取流

1
2
3
4
5
List<Integer> age = new ArrayList<>();
age.add(20);
age.add(19);
age.add(22);
Stream<Integer> ageStream = age.stream();

Set 集合获取流

1
2
3
4
5
Set<String> name = new HashSet<>();
name.add("张三");
name.add("李四");
name.add("王五");
Stream<String> nameStream = name.stream();

Map 集合转换后获取流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Map<String,Integer> map = new HashMap<>();
map.put("张三",18);
map.put("李四",19);
map.put("王五",16);

// map 中的 keySet 获取流
Set<String> keySet = map.keySet();
Stream<String> keySetStream = keySet.stream();

// map 中的 values 获取流
Collection<Integer> values = map.values();
Stream<Integer> valuesStream = values.stream();

// map 中的 entrySet 获取流
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Stream<Map.Entry<String, Integer>> stream = entries.stream();

根据数组获取流

1
2
3
4
// 数组获取流
Stream<Integer> integerStream = Stream.of(1, 3, 5, 78, 1);
int[] arr = {1,3,5,6,8};
Stream<int[]> arr1 = Stream.of(arr);

Stream 流中的方法

  Stream 流模型的操作很丰富,这里介绍一些常用的 API。
  对这些方法而言,又可以被分成 2 类:

  • 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用;
  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不在支持类似StringBuilder那样的链式调用

延迟方法

  延迟方法包括:filtermaplimitskipconcat

过滤:filter

  filter方法接受一个Predicate函数式接口,能将一个流转换为另一个子集流:

1
Stream<T> filter(Predicate<? super T> predicate);

  示例代码:

1
2
3
4
5
6
7
@Test
public void testFilterOfStream() {
Stream.of("张三", "李四", "王五", "王老五")
.filter(name -> name.startsWith("王"))
.filter(name -> name.length() == 2)
.forEach(name -> System.out.println(name));
}

  测试结果:

1
王五

映射:map

  map方法接受一个Function函数式接口,可以将流中的元素映射到另一个流中:

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

  示例代码:

1
2
3
4
5
6
@Test
public void testStreamMap() {
Stream.of("2131", "141", "8888", "66666")
.map(number -> Integer.parseInt(number))
.forEach(number -> System.out.println(number));
}

  测试结果:

1
2
3
4
2131
141
8888
66666

截取:limit

  limit方法可以对流进行截取,只取用前n个:

1
Stream<T> limit(long maxSize);

  示例代码:

1
Stream.of(1, 3, 6, 5, 8, 11).limit(3).forEach(number -> System.out.println(number));

  运行结果:

1
2
3
1
3
6

跳过:skip

  若你希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流(若流的当前长度大于 n,则跳过前 n 个;否则将得到一个长度为 0 的新流):

1
Stream<T> skip(long n);

  示例代码:

1
Stream.of(1, 3, 6, 5, 8, 11).skip(3).forEach(number -> System.out.println(number));

  运行结果:

1
2
3
5
8
11

组合:concat

  concat方法可以将两个流合并成为一个流:

1
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {...}

  示例代码:

1
2
3
4
Stream<Integer> stream1 = Stream.of(1, 3, 5, 7, 9);
Stream<Integer> stream2 = Stream.of(2, 4, 6, 8, 10);
Stream<Integer> concat = Stream.concat(stream1, stream2);
concat.forEach(number -> System.out.println(number));

  运行结果:

1
2
3
4
5
6
7
8
9
10
1
3
5
7
9
2
4
6
8
10

终结方法

  终结方法包括:forEachcount

逐一处理:forEach

  虽然方法名字叫forEach,但是与 for 循环中的 for-each 称呼不同。
  该方法接受一个Consumer函数式接口,会将每一个流元素交给该函数进行处理:

1
void forEach(Consumer<? super T> action);

  示例代码:

1
Stream.of("张三","李四","王五").forEach(name -> System.out.println(name));

  运行结果:

1
2
3
张三
李四
王五

统计个数:count

  正如旧集合Collection当中的size方法一样,Stream 流提供count方法来统计流中的元素个数,该方法返回一个 long 值代表元素个数:

1
long count();

  示例代码:

1
2
long count = Stream.of(1, 3, 6, 5, 8, 11).count();
System.out.println(count);

  运行结果:

1
6

注意哦Stream流属于管道流,只能被使用一次,第一个Stream流的方法调用完毕后,数据就会转到下一个Stream流上,这时第一个Stream流就使用完毕被关闭,不能在调用方法了。

方法引用

  在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。
  那么现在考虑一种情况:若我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
  先看一个栗子:

冗余的 Lambda 代码

  例如下面的代码:

1
2
3
public interface Student {
void work(String str);
}

  Student接口当中唯一的抽象方法work接受一个String类型的参数,可以输入学生具体的工作内容。
  若使用 Lambda 表达式方式:

1
2
3
4
5
6
7
8
9
10
public void getWork(Student student) {
student.work("学生的工作是学习。。。。");
}

@Test
public void testLambda() {
getWork(s -> System.out.println(s));
}
//运行结果
学生的工作是学习。。。。

问题分析

  上面代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是System.out对象中的println(s)方法。
  既然 Lambda 希望做的事情就是调用println(s)方法,那何必自己手动调用呢?

方法引用改进代码

  能否省去 Lambda 的语法格式(尽管它已经相当简洁)呢?
  可以的,我们只要“引用”过去就好了,代码简化如下:

1
getWork((System.out::println));

  双冒号::为引用运算符,而它所在的表达式被称为方法引用

方法引用种类

  从这些例子可以看出, 要用:: 操作符分隔对象与方法名或类名与方法名。主要有 3 种情况:

  • object::instanceMethod:对象名引用成员方法;
  • Class::static Method:类名引用静态成员方法
  • Class::instanceMethod:类名引用成员方法

  在前 2 种情况中, 方法引用等价于提供方法参数的 Lambda 表达式。
  对于第 3 种情况, 第 1 个参数会成为方法的目标。
注:若有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。

对象名引用成员方法

  前面已经提到,因为System.out对象中有一个重载的println(String)方法恰好就是我们所需要的,所以下面两行代码等价:

1
2
System.out::println
s -> System.out.println(s)

  该对象为系统自带,看起来可能没那么直观,我们自己创建一个对象来举个例子:

1
2
3
4
5
6
7
8
9
10
11
// 学生接口
public interface Student {
void work(String str);
}
// 对象
public class MethodReferenctByObject {
public void noGame(String str){
str += "不要沉迷游戏";
System.out.println(str);
}
}

  下面为分别使用 Lambda 表达式和方法引用的方式写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void getWork(Student student) {
student.work("学生的工作是学习。。。。");
}


@Test
public void testStudent(){
// Lambda 表达式
getWork(str -> {
MethodReferenctByObject methodRefByObj = new MethodReferenctByObject();
methodRefByObj.noGame(str);
});
// 方法引用
MethodReferenctByObject methodRefByObj= new MethodReferenctByObject();
getWork(methodRefByObj::noGame);
}

  运行结果都为:

1
学生的工作是学习。。。。不要沉迷游戏

类名引用成员方法

  类似地, 下面两行代码等价:

1
2
Math::pow
(x,y) -> Math.pow(x, y)

类名引用静态成员方法

  类似地, 下面两行代码等价:

1
2
String::compareToIgnoreCase
(x, y) -> x.compareToIgnoreCase(y)

其他方法引用

this 引用本类的成员方法

  对于接口中的方法,我们可以直接重写代码,也可以在其中通过this调用本类的方法。下面有一个购买的方法,现在有一个消费者类通过该接口去购物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 接口
public interface Buy {
void buy();
}
// 消费者类
public class Consumer {
public void shopping(Buy buy) {
buy.buy();
}

public void buySomething() {
System.out.println("去超市买点生活用品");
}
}

  下面分别为 Lambda 表达式方式和方法引用的测试代码:

1
2
3
4
5
6
7
8
9
10
@Test
public void testLambda() {
shopping(() -> {
this.buySomething();
});
}
@Test
public void testMethodRef(){
shopping(this::buySomething);
}

  运行结果都为:

1
去超市买点生活用品

super 引用父类的成员方法

  类似于this引用本类的成员方法,只是把当前类为子类且继承了一个父类,此处代码省略。

类的构造器引用

  由于构造器的名称与类名完全一样,并不固定,所以构造器引用类名称::new的格式表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Person 类
public class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}
// Person 接口,根据姓名返回 Person
public interface PersonInterface {
Person getPersonName(String name);
}
// 测试 Person 类
public class TestPerson {
public void getPerson(String name, PersonInterface personInter) {
Person person = personInter.getPersonName(name);
System.out.println(person.getName());
}
}

  下面分别为Lambda表达式方式和方法引用的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
    @Test
public void testLambda() {
getPerson("王五", name -> new Person(name));
}

@Test
public void testMethodRer(){
getPerson("王五",Person::new);
}

// 运行结果都为
王五

数组的构造器引用

  数组也是Object的子类对象,所以同样具有构造器,只是语法稍有不同。
  若对应到 Lambda 的使用场景中时,需要一个函数式接口:

1
2
3
4
// 根据长度返回一个新的数组
public interface ArrayInterface {
int[] getArrayBylength(int lenth);
}

  下面分别为 Lambda 表达式方式和方法引用的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestArray {
public void getArray(int length, ArrayInterface arrayInterface) {
int[] arrayBylength = arrayInterface.getArrayBylength(length);
}

@Test
public void testLambda() {
getArray(10, length -> new int[length]);
}

@Test
public void testMethodRef() {
getArray(10, int[]::new);
}
}

文章信息

时间 说明
2020-07-08 部分文字描述修正
0%